import optparse
import os
import re
import subprocess
from xml.etree.ElementTree import XMLParser

import util


DOCBOOK_HEADER_PAT = u"""<?xml version="1.0"?>
<!DOCTYPE %s PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
"""

BOOK_XML_FILENAME = 'book.xml'

DEFAULT_BOOK_XML_HEADER = u"""<?xml version="1.0"?>
<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">

<book id="I_book_d1e1">
  <title><!-- INSERT TITLE HERE --></title>
"""

DEFAULT_BOOK_XML_FOOTER = u"""
</book>
"""

RE_CHAPTER_FNAME_STR = r'ch(\d+)(_[^\.]*)?\.xml'

RE_CHAPTER_FNAME = re.compile(RE_CHAPTER_FNAME_STR)

RE_BOOK_XML_CHAPTER = re.compile(util.RE_BOOK_XML_CHAPTER_PAT % RE_CHAPTER_FNAME_STR)

BOOK_XML_CHAPTER_PAT = u'  <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="%s"/>'


class OPMLParser(object):
    def __init__(self):
        self._depth = 0
        self._current_section = None
        self._section_ids = set()
        self.top_sections = []

    def start(self, tag, attrib):
        self._depth += 1
        if tag == 'outline':
            title = unicode(attrib.get('text', u''))

            section_id = re.sub(r'\W', '_', title)
            full_section_id = section_id
            count = 1
            while full_section_id in self._section_ids:
                full_section_id = section_id + '_' + str(count)
            self._section_ids.add(full_section_id)

            text = unicode(attrib.get('_note', u''))
            section = util.Section(full_section_id, title, text)

            if self._current_section:
                self._current_section.add_section(section)
            self._current_section = section
            if self._depth == 3:
                self.top_sections.append(section)

    def end(self, tag):
        self._depth -= 1
        if tag == 'outline':
            self._current_section = self._current_section.parent

    def data(self, data):
        pass

    def close(self):
        pass


def parse_opml(data):
    opml_parser = OPMLParser()
    xml_parser = XMLParser(target=opml_parser)
    xml_parser.feed(data)
    xml_parser.close()
    return opml_parser.top_sections


def auto_para(text):
    regexp_parts = util.RE_BLOCK_ELEM.split(text)
    parts = []
    for i in xrange(0, len(regexp_parts), 3):
        parts.append(regexp_parts[i])
        if i < len(regexp_parts) - 1:
            parts.append(regexp_parts[i+1])
    
    for i in xrange(0, len(parts), 2):
        paragraphs = util.RE_PARA_BREAK.split(parts[i])
        paragraphs = [p.strip() for p in paragraphs if p.strip()]
        if paragraphs:
            parts[i] = ('<para>' +
                        '</para>\n\n<para>'.join(paragraphs) +
                        '</para>')
        else:
            parts[i] = ''
    text = '\n\n'.join(parts).strip()
    if not text:
        # Empty sections need at least one element to be valid.
        text = '<para></para>'

    return text


def get_docbook_text_for_section(section, depth=0):
    if depth == 0:
        root_elem = 'preface' if section.title == 'Preface' else 'chapter'
    else:
        root_elem = 'sect' + str(depth)

    parts = ['\n<%s id="%s">' % (root_elem, section.section_id),
             '<title>%s</title>\n' % (section.title),
             auto_para(section.text)]
    for section in section.inner_sections:
        parts.append(get_docbook_text_for_section(section, depth=depth+1))
    parts.append('\n</%s>\n' % root_elem)

    return '\n'.join(parts)


def make_chapter_files(root_dir, top_sections):
    chapter_filenames = []
    for (i, section) in enumerate(top_sections):
        fname = 'ch%02d_%s.xml' % (i, section.section_id)
        chapter_filenames.append(fname)
        full_path = os.path.join(root_dir, fname)
        fh = open(full_path, 'w')
        root_elem = 'preface' if section.title == 'Preface' else 'chapter'
        fh.write((DOCBOOK_HEADER_PAT % root_elem).encode('utf-8'))
        fh.write(get_docbook_text_for_section(section).encode('utf-8'))
        fh.close()

    return chapter_filenames


def update_book_xml(output_dir, chapter_filenames):
    book_path = os.path.join(output_dir, BOOK_XML_FILENAME)
    if os.path.isfile(book_path):
        fh = open(book_path)
        header_lines = []
        for line in fh:
            if RE_BOOK_XML_CHAPTER.match(line):
                break
            header_lines.append(line)
        footer_lines = []
        for line in fh:
            if RE_BOOK_XML_CHAPTER.match(line):
                continue
            footer_lines.append(line)
        fh.close()

        header = ''.join(header_lines)
        footer = ''.join(footer_lines)

    else:
        header = DEFAULT_BOOK_XML_HEADER
        footer = DEFAULT_BOOK_XML_FOOTER

    fh = open(book_path, 'w')
    fh.write(header.encode('utf-8'))
    for chapter_fname in chapter_filenames:
        fh.write((BOOK_XML_CHAPTER_PAT % chapter_fname).encode('utf-8'))
        fh.write('\n')
    fh.write(footer.encode('utf-8'))
    fh.close()


def svn_assist(output_dir, chapter_filenames, old_chapter_filenames):
    is_svn_dir = os.path.isdir(os.path.join(output_dir, '.svn'))

    existing_chapter_filenames = [p for p in os.listdir(output_dir)
                               if RE_CHAPTER_FNAME.match(p)]
    files_to_add = [p for p in chapter_filenames
                    if p not in old_chapter_filenames]
    files_to_delete = [p for p in existing_chapter_filenames
                       if p not in chapter_filenames]

    if is_svn_dir and (files_to_add or files_to_delete):
        print 'Run these commands to adjust add and remove files in SVN:'
        for p in files_to_add:
            print 'svn add %s' % p
        for p in files_to_delete:
            print 'svn delete %s' % p

    if not is_svn_dir and files_to_delete:
        print 'Run these commands to remove extraneous chapter files:'
        for p in files_to_delete:
            print 'rm %s' % p


def validate_xml(output_dir, schema_dir, xmllint_command):
    catalog_path = os.path.join(schema_dir, 'catalog.xml')
    if not os.path.isfile(catalog_path):
        print 'Could not validate XML: No DocBook schema catalog found in %s.'
        return 1

    if not xmllint_command:
        xmllint_command = 'xmllint'
        
    os.environ['XML_CATALOG_FILES'] = catalog_path
    os.environ['SGML_CATALOG_FILES'] = catalog_path
    xmllint_args = [xmllint_command, '--xinclude', '--postvalid', '--catalogs',
                    '--noout', os.path.join(output_dir, 'book.xml')]
    try:
        return subprocess.call(xmllint_args)
    except OSError, e:
        print ('Could not validate XML.  Maybe xmllint could not be found?\n'
               'xmllint command: %s\nOSError: %s' % (xmllint_command, e))
        return 1


def get_option_parser():
    parser = optparse.OptionParser(
        usage='%prog [options] <OPML-input-file> <DocBook-output-directory>')
    parser.add_option('--schema-dir', dest='schema_dir',
                      help='The directory containing DocBook schema files '
                      '(catalog.xml and others).')
    parser.add_option('--xmllint-command', dest='xmllint_command',
                      help='The path to the xmllint command.  You only need '
                      'to specify this if xmllint is not on your command path.')
    return parser


def main(args):
    opt_parser = get_option_parser()
    (options, args) = opt_parser.parse_args(args)

    if len(args) != 2:
        print 'Incorrect number of arguments.'
        opt_parser.print_help()
        return 1

    opml_filename = args[0]
    output_dir = args[1]
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    if not os.path.isdir(output_dir):
        print 'DocBook output directory %s is not a directory.' % output_dir
        opt_parser.print_help()
        return 1

    schema_dir = options.schema_dir or os.getcwd()
    if not os.path.isdir(schema_dir):
        print 'DocBook directory %s is not a directory.' % schema_dir
        opt_parser.print_help()
        return 1

    do_validate = True
    if not os.path.isfile(os.path.join(schema_dir, 'catalog.xml')):
        print ('Warning: Could not find DocBook schema catalog.xml file.\n'
               'Use the --schema-dir argument to set the DocBook schema\n'
               'directory.  Skipping XML validation.')
        do_validate = False

    fh = open(opml_filename)
    top_sections = parse_opml(fh.read())
    fh.close()

    if len(top_sections) == 1 and top_sections[0].title == 'Draft':
        top_sections = top_sections[0].inner_sections

    old_chapter_filenames = [p for p in os.listdir(output_dir)
                             if RE_CHAPTER_FNAME.match(p)]
    chapter_filenames = make_chapter_files(output_dir, top_sections)
    update_book_xml(output_dir, chapter_filenames)
    svn_assist(output_dir, chapter_filenames, old_chapter_filenames)

    result = validate_xml(output_dir, schema_dir,
                          xmllint_command=options.xmllint_command)
    if result != 0:
        print '\n*** There was an error validating the XML.'
        return result

    return 0
